/* eslint-disable no-restricted-syntax */
import { MarkdownParser } from '../utils/markdownParser'
import { TextFileReader } from '../utils/textFileReader'
import { TextHelper } from '../utils/textHelper'
import { MarkdownElement, MarkdownElementType } from './markdownModel'
import { ReadContext } from './readerContext'

type ObjectValueType = null | string | number | boolean

/**
 * Main functions:
 * read a markdown file into an object
 */
export class MarkdownReader {
	constructor(public path: string) {}

	private static createHeader(array: RegExpExecArray, context: ReadContext) {
		const element: MarkdownElement = {
			type: MarkdownElementType.header,
			level: array[1].length,
			name: array[2],
		}
		if (element.level <= 1) {
			return
		}
		if (element.level <= context.currentLevel) {
			context.pop(element.level - 1)
		}
		let propertyName = MarkdownReader.normalize(element.name, true) as string
		// case: the property is an array
		if (propertyName.endsWith('[]')) {
			propertyName = propertyName.substring(0, propertyName.length - 2).trim()
		}
		// generate the property
		context.push(propertyName, null)
	}

	private static createListItem(array: RegExpExecArray, context: ReadContext) {
		const element: MarkdownElement = {
			type: MarkdownElementType.listItem,
			level: 0,
			name: array[1],
			value: array[2],
		}
		// TODO: case: multiple lines value
		context.setCurrentProperty(
			MarkdownReader.normalize(element.name, true) as string,
			MarkdownReader.normalize(element.value!),
		)
	}

	private static handleHeader(line: string, reader: TextFileReader, context: ReadContext): boolean {
		if (!line) {
			return true
		}
		const result = MarkdownParser.matchHeader(line)
		if (result) {
			MarkdownReader.createHeader(result, context)
			return true
		}
		return false
	}

	private static handleList(line: string, reader: TextFileReader, context: ReadContext): boolean {
		if (!line) {
			return true
		}
		const result = MarkdownParser.matchListItem(line)
		if (result) {
			MarkdownReader.createListItem(result, context)
			return true
		}
		return false
	}

	private static handleTable(line: string, reader: TextFileReader, context: ReadContext): boolean {
		if (!line) {
			// case: end of file
			return true
		}

		let isValid = MarkdownParser.testTableHeader(line)
		if (!isValid) {
			return false
		}

		// check if the next line is table line row
		const headerLine = line
		let dataLine = reader.peak()
		if (!dataLine) {
			// case: end of file
			return true
		}
		isValid = MarkdownParser.testTableLine(dataLine)
		if (!isValid) {
			return false
		}

		reader.move()
		context.current = []
		const headers: string[] = MarkdownReader.normalizeTableRow(headerLine, true) as string[]
		const isArray = MarkdownReader.isArrayTable(headers)
		// now: it is a table, read table rows
		while (reader.hasMore(1)) {
			dataLine = reader.peak()!.trim()
			if (!dataLine) {
				// case: end of file
				return true
			}

			isValid = MarkdownParser.testTableRow(dataLine)
			if (!isValid) {
				// true: indicate we use these read lines
				return true
			}

			reader.move()

			// read a data row
			const cells: ObjectValueType[] = MarkdownReader.normalizeTableRow(dataLine)

			if (isArray) {
				if (!context.current) {
					context.current = []
				}
				context.current.push(cells)
			} else {
				// data row -> data row object
				const rowObject: any = {}
				headers.some((header, index): boolean => {
					if (index >= cells.length) {
						return true
					}
					rowObject[header] = cells[index]
					return false
				})

				// put into the result
				if (!MarkdownReader.mergeTableRow(headers, rowObject, context)) {
					context.current.push(rowObject)
				}
			}
		}

		return false
	}

	private static isArrayTable(headers: string[]): boolean {
		for (const element of headers) {
			if (element.length > 0) {
				return false
			}
		}
		return true
	}

	private static mergeTableRow(headers: string[], rowObject: any, context: ReadContext): boolean {
		if (
			context.current.length === 0 ||
			rowObject[headers[0]] !== '' ||
			rowObject[headers[1]] !== ''
		) {
			return false
		}
		const lastObject: any = context.current[context.current.length - 1]
		headers.forEach((header) => {
			const prop = rowObject[header]
			if (prop) {
				// TODO: merge with \r\n
				lastObject[header] = `${lastObject[header]} ${prop}`
			}
		})
		return true
	}

	public static normalize(value: string, isPropertyName: boolean = false): ObjectValueType {
		if (!value) {
			return value
		}

		let newValue: ObjectValueType = TextHelper.decodeMarkdown(value.trim())

		// case: inline code
		if (newValue.length >= 2 && newValue[0] === '`' && newValue[newValue.length - 1] === '`') {
			const blockValue = newValue.substring(1, newValue.length - 1)
			if (!isPropertyName) {
				if (blockValue.toLocaleLowerCase() === 'yes' || blockValue.toLocaleLowerCase() === 'true') {
					newValue = true
				} else if (
					blockValue.toLocaleLowerCase() === 'no' ||
					blockValue.toLocaleLowerCase() === 'false'
				) {
					newValue = false
				} else if (blockValue.toLocaleLowerCase() === 'null') {
					newValue = null
				} else {
					const testValue: number = parseFloat(blockValue)
					// eslint-disable-next-line no-restricted-globals
					if (!isNaN(testValue)) {
						newValue = testValue
					}
				}
			} else {
				newValue = newValue.substring(1, newValue.length - 1)
			}
		}

		// case: produce a good property name
		if (isPropertyName && typeof newValue === 'string' && newValue.length > 0) {
			// case: remove spaces
			newValue = newValue.replace(' ', '')
			// case: camelCase
			newValue = newValue[0].toLocaleLowerCase() + newValue.substr(1, newValue.length)
		}
		return newValue
	}

	private static normalizeTableRow(line: string, isName: boolean = false): ObjectValueType[] {
		const values = MarkdownParser.splitTableRowToCells(line)
		return values.map((value) => MarkdownReader.normalize(value, isName))
	}

	private static readLine(line: string, reader: TextFileReader, context: ReadContext) {
		if (MarkdownReader.handleHeader(line, reader, context)) {
			return
		}
		if (MarkdownReader.handleList(line, reader, context)) {
			return
		}

		MarkdownReader.handleTable(line, reader, context)
	}

	public toObject(): any {
		const reader = new TextFileReader()
		const context = new ReadContext()
		reader.open(this.path)

		while (reader.hasMore()) {
			MarkdownReader.readLine(reader.read()!.trim(), reader, context)
		}

		return MarkdownReader.upgrade(context.root)
	}

	/**
	 * convert the markdown object to the model object
	 * @param table the markdown object
	 * @returns the dbTable object
	 */
	private static upgrade(table: any) {
		if (table.name) {
			return MarkdownReader.upgradeV1(table)
		}

		return MarkdownReader.upgradeV2(table)
	}

	/**
	 * Version 1
	 * @param table the markdown object
	 * @returns the dbTable object
	 */
	private static upgradeV1(table: any) {
		return {
			name: table.name,
			comment: table.comment,
			primaryKey: table.primaryKey,
			indexes: table.indexes || [],
			foreignKeys: table.foreignKeys || [],
			columns: table.columns || [],
		}
	}

	/**
	 * Version 2
	 * @param table the markdown object
	 * @returns the dbTable object
	 */
	private static upgradeV2(table: any) {
		const basic: any = table.table && table.table.length > 0 ? table.table[0] : undefined
		return {
			...basic,
			primaryKey: table.primaryKey?.length > 0 ? table.primaryKey[0] : undefined,
			indexes: table.indexes || [],
			foreignKeys: table.foreignKeys || [],
			columns: table.columns || [],
		}
	}
}
